ANDROID ARCHITECTURE
COMPONENTS · v2.8+
ANDROIDX LIFECYCLE
ViewModel · ViewModelStore · ViewModelStoreOwner

ViewModel
Architecture

The complete internals — from survival to scoping

ViewModel is more than a class that survives rotation. Understanding its full architecture — the Store, the Owner, the Factory chain, and the scoping system — turns it from a magic box into a tool you truly control.

ViewModel ViewModelStore ViewModelStoreOwner ViewModelProvider SavedStateHandle Hilt injection
02 · The Problem

Why ViewModel exists

Before ViewModel, every configuration change (screen rotation, split-screen resize, locale change) destroyed and recreated your Activity or Fragment. UI-related data had to be serialized into a Bundle, which was limited to primitives and Parcelables, and re-fetched from disk/network after every rotation. A loading spinner, an in-progress network call, a 500-item list — all lost.

ViewModel solves a single, specific problem: survive configuration changes without serializing data through a Bundle. Everything else it does — scoping data to UI, separating concerns — is a consequence of this core capability.

The precise contract: A ViewModel is created when its owning scope first requests it. It survives any number of configuration changes to that scope. It is destroyed — and its onCleared() called — only when the scope is permanently finished: the Activity is finishing (not rotating), the Fragment is removed from the back stack, or the NavGraph scope is popped.

Before ViewModel
Activity (portrait) → destroyed
Bundle (serialized) → passed
Activity (landscape) → recreated
↳ re-fetch data · lose in-flight calls
vs
With ViewModel
Activity (portrait) → destroyed
ViewModel → survives in ViewModelStore
Activity (landscape) → recreated
↳ same ViewModel instance returned
03 · Survival

The ViewModel Lifecycle

The most important thing to internalize: a ViewModel's lifetime is longer than the lifetime of any single Activity or Fragment instance. Its lifetime is scoped to the logical existence of that UI — all rotations included — not to a single object instance.

LIFECYCLE SIMULATION — Activity + ViewModel
Activity instance
onCreate()
onStart()
onResume() ← active
ViewModel
MyViewModel
alive · data intact
Press "Rotate device" to simulate a configuration change.

onCleared() — the final callback

This is the ViewModel's only lifecycle callback. It fires exactly once, when the owning scope is permanently done. Use it to cancel coroutines that were started outside viewModelScope — though you should rarely need this since viewModelScope auto-cancels in onCleared().

MainViewModel.kt
class MainViewModel : ViewModel() {

    // viewModelScope is automatically cancelled in onCleared()
    init {
        viewModelScope.launch {
            repo.streamUpdates().collect { update ->
                _state.update { it.copy(data = update) }
            }
        }
    }

    // Rare — only for non-viewModelScope resources
    private val bluetoothListener = BluetoothListener()

    override fun onCleared() {
        super.onCleared()
        // Called ONCE, only when scope is permanently destroyed
        // NOT called on rotation
        bluetoothListener.unregister()
    }
}

addCloseable(Closeable): Added in lifecycle 2.5. Attach any Closeable resource — it will be automatically closed in onCleared(). Cleaner than overriding onCleared() for resource cleanup.

04 · Internals

ViewModelStore

This is the mechanism that makes ViewModel survival possible. ViewModelStore is a simple in-memory map that holds ViewModel instances, keyed by a canonical string. It lives attached to the ViewModelStoreOwner — and the system ensures the Store is carried across configuration changes while the Owner is recreated.

ViewModelStore — internal HashMap
"androidx.lifecycle.VIEW
MODEL_KEY:com.app.MainViewModel"
MainViewModel@7f3a
"androidx.lifecycle.VIEW
MODEL_KEY:com.app.ProfileViewModel"
ProfileViewModel@4c2b
"custom-key:SharedVM"
SharedViewModel@9d1e
Key facts
Keys are built from canonical class name + optional custom key
Values are ViewModel instances — alive in memory
Store is retained by system across config changes
Store.clear() calls onCleared() on all VMs then empties map
One Store per ViewModelStoreOwner (Activity, Fragment…)
ViewModelStore.kt (source)
// Simplified from AndroidX source — it really is this simple
class ViewModelStore {

    private val map = HashMap<String, ViewModel>()

    fun put(key: String, viewModel: ViewModel) {
        val oldVM = map.put(key, viewModel)
        oldVM?.onCleared()   // clear replaced VM
    }

    fun get(key: String): ViewModel? = map[key]

    fun keys(): Set<String> = map.keys.toHashSet()

    // Called by the owner when it's truly finishing
    fun clear() {
        for (vm in map.values) vm.onCleared()
        map.clear()
    }
}

// The key format used by ViewModelProvider
private const val DEFAULT_KEY =
    "androidx.lifecycle.ViewModelProvider.DefaultKey"
// Full key: "$DEFAULT_KEY:${viewModelClass.canonicalName}"

How the Store survives rotation

ComponentActivity (the base of AppCompatActivity) implements a retention mechanism using NonConfigurationInstances. When Android destroys an Activity for a configuration change, it calls onRetainNonConfigurationInstance() before destruction. The returned object is preserved by the Activity runtime and passed back to the new Activity instance via getLastNonConfigurationInstance(). The ViewModelStore is stored inside this object.

Rotation
detected
onRetainNon
ConfigInstance()
called before destroy
ViewModelStore
saved in bundle
retained by system
Activity
destroyed &
recreated
getLast
NonConfigInstance()
called in onCreate
Same Store
returned
same VMs inside

Fragment's mechanism: Fragments use a different approach — FragmentManagerViewModel, a ViewModel scoped to the parent (Activity or parent Fragment) that holds a map of child Fragment ViewModelStores. This nesting is what enables by activityViewModels() to return the Activity-scoped instance.

05 · The Interface

ViewModelStoreOwner

ViewModelStoreOwner is a single-method interface. Any class implementing it declares: "I own a ViewModelStore, and I manage its lifecycle." The implementer is responsible for clearing the store when its lifecycle ends permanently.

ViewModelStoreOwner.kt
// The entire interface — just one property
interface ViewModelStoreOwner {
    val viewModelStore: ViewModelStore
}

// ComponentActivity implements it like this:
class ComponentActivity : ViewModelStoreOwner, ... {

    private var _viewModelStore: ViewModelStore? = null

    override val viewModelStore: ViewModelStore
        get() {
            if (_viewModelStore == null) {
                // Try to restore from non-config instance (rotation)
                val nc = lastNonConfigurationInstance
                if (nc is NonConfigurationInstances) {
                    _viewModelStore = nc.viewModelStore
                }
                if (_viewModelStore == null) {
                    _viewModelStore = ViewModelStore()  // fresh start
                }
            }
            return _viewModelStore!!
        }

    override fun onDestroy() {
        super.onDestroy()
        if (!isChangingConfigurations()) {
            // Permanently finishing — clear the store
            viewModelStore.clear()
        }
        // If isChangingConfigurations() — store is kept!
    }
}

All built-in ViewModelStoreOwners

ComponentActivity
AppCompatActivity · FragmentActivity
until Activity.finish()
The most common owner. Its Store is cleared when isFinishing() is true in onDestroy — which is when the user navigates away, not when rotating. Survives: rotation, split-screen, multi-window resize, dark mode toggle.
Fragment
DialogFragment · BottomSheetFragment
until Fragment detaches
Fragment is itself a ViewModelStoreOwner. Its store is cleared when the Fragment is removed from the back stack and its onDestroy is called without being re-added. Note: going on the back stack does NOT clear the store.
NavBackStackEntry
Jetpack Navigation Component
until destination popped
Each destination in a NavGraph is a ViewModelStoreOwner. This enables sharing a ViewModel across multiple fragments within a navigation sub-graph — cleared only when the entry is popped off the back stack entirely.
Custom Owner
Your own implementation
custom lifetime
Implement ViewModelStoreOwner on any class — a custom View, a service context wrapper, a Composable-backed scope — to get a ViewModel whose lifetime you control precisely.
06 · Retrieval

ViewModelProvider & Factories

ViewModelProvider is the gatekeeper. It bridges the request for a ViewModel with the Store and the Factory. Its job is simple: look up the key in the Store; if found, return the existing instance; if not, use the Factory to create one and put it in the Store.

ViewModelProvider internals
// Simplified ViewModelProvider.get() logic
fun <T : ViewModel> get(modelClass: KClass<T>): T {
    val canonicalName = modelClass.java.canonicalName
        ?: throw IllegalArgumentException("Local and anonymous classes can't be ViewModels")
    val key = "$DEFAULT_KEY:$canonicalName"
    return get(key, modelClass)
}

fun <T : ViewModel> get(key: String, modelClass: KClass<T>): T {
    val viewModel = store.get(key)
    if (modelClass.isInstance(viewModel)) {
        return viewModel as T   // ← found in store, return existing
    }
    // Not found — create via factory
    val newVM = factory.create(modelClass, extras)
    store.put(key, newVM)
    return newVM
}

// Usage variants

// 1. No-arg ViewModel (Hilt or reflection-based)
val vm: MainViewModel by viewModels()

// 2. Custom factory (manual DI)
val vm: MainViewModel by viewModels {
    MainViewModelFactory(repository)
}

// 3. Custom key (multiple instances of same class)
val vm1 = ViewModelProvider(this).get("player_1", PlayerViewModel::class)
val vm2 = ViewModelProvider(this).get("player_2", PlayerViewModel::class)

The Factory hierarchy

When no explicit factory is provided, ViewModelProvider uses the application-level factory. Modern Hilt integration puts an HiltViewModelFactory here via the @AndroidEntryPoint annotation, enabling zero-boilerplate dependency injection.

NewInstanceFactory
The base factory — creates ViewModels using reflection on a no-arg constructor. Throws if no public no-arg constructor exists.
AndroidViewModelFactory
Extends NewInstanceFactory. Also handles AndroidViewModel(application) by passing the Application context automatically.
SavedStateViewModelFactory
Handles SavedStateHandle injection. Created by ComponentActivity and Fragment as the default factory. Wraps whatever factory you provide.
HiltViewModelFactory
Injected by Hilt's @AndroidEntryPoint. Delegates to the Hilt DI graph for construction — enables full constructor injection with @Inject.
CreationExtras
Lifecycle 2.5+. Type-safe key-value map passed to factories. Replaces constructor parameters on the factory itself. Use APPLICATION_KEY, SAVED_STATE_REGISTRY_OWNER_KEY, etc.
Modern Factory with CreationExtras (Lifecycle 2.5+)
class MyViewModelFactory(
    private val repo: UserRepository
) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(
        modelClass: Class<T>,
        extras: CreationExtras
    ): T {
        // Extract standard extras
        val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!
        val savedStateHandle = extras.createSavedStateHandle()

        return when (modelClass) {
            MainViewModel::class.java ->
                MainViewModel(repo, savedStateHandle) as T
            else -> throw IllegalArgumentException("Unknown VM class")
        }
    }

    companion object {
        // The modern recommended factory pattern
        val Factory = viewModelFactory {
            initializer {
                val app = this[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]!!
                val handle = createSavedStateHandle()
                val repo = (app as MyApp).appComponent.userRepository
                MainViewModel(repo, handle)
            }
        }
    }
}
07 · Scoping

Scoping Strategies

Every call to ViewModelProvider(owner)[MyViewModel::class] returns the same instance for that owner. Change the owner and you get a different (or new) ViewModel. This means ViewModel sharing is entirely a question of which owner you pass.

Activity scope
by activityViewModels()
widest

All Fragments in this Activity share one instance. Lifetime = until the Activity is truly finished. Use for data that must flow between unrelated sibling screens.

NavGraph scope
by navGraphViewModels(R.id.checkout_graph)
flow

Only Fragments within that sub-graph share the instance. Cleared when the entire sub-graph is popped. Perfect for multi-step flows (checkout, onboarding) where you need state across multiple screens.

Fragment scope
by viewModels()
screen

Private to this Fragment. Not shared with siblings or parent. Cleared when the Fragment is permanently destroyed (removed and not re-added). The most common scope for single-screen ViewModels.

Custom owner scope
ViewModelProvider(customOwner)[MyVm::class]
custom

Pass any ViewModelStoreOwner. In Compose: use hiltViewModel() or create a custom owner for non-standard scoping. In tests: provide a fake owner to control ViewModel lifetime precisely.

All scoping options
// 1. Fragment-scoped (private to this fragment)
val vm: ProfileViewModel by viewModels()

// 2. Activity-scoped (shared with all siblings)
val sharedVm: SharedViewModel by activityViewModels()

// 3. NavGraph-scoped (shared within checkout flow)
val checkoutVm: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)

// 4. NavBackStackEntry-scoped (explicit destination)
val entry = findNavController().getBackStackEntry(R.id.cartFragment)
val cartVm: CartViewModel by viewModels({ entry })

// 5. Multiple instances of the same VM class
val player1Vm = ViewModelProvider(this).get("player_1", PlayerViewModel::class.java)
val player2Vm = ViewModelProvider(this).get("player_2", PlayerViewModel::class.java)

// 6. Compose — hiltViewModel() uses LocalViewModelStoreOwner
@Composable
fun ProfileScreen() {
    val vm: ProfileViewModel = hiltViewModel()          // NavEntry scope
    val sharedVm: SharedViewModel = hiltViewModel(       // Activity scope
        viewModelStoreOwner = LocalContext.current as ViewModelStoreOwner
    )
}
08 · Dependency Injection

Hilt Integration

Hilt's ViewModel integration is the modern standard. @HiltViewModel on the ViewModel class plus @Inject constructor enables full dependency injection with zero factory boilerplate. Hilt registers a custom ViewModelProvider.Factory through @AndroidEntryPoint that builds the DI graph for you.

Hilt ViewModel — full setup
// ViewModel — annotate with @HiltViewModel, inject with @Inject
@HiltViewModel
class OrderViewModel @Inject constructor(
    private val orderRepo: OrderRepository,
    private val userRepo: UserRepository,
    private val analytics: AnalyticsTracker,
    private val savedState: SavedStateHandle   // auto-injected by Hilt
) : ViewModel() {

    val orders = orderRepo.getOrders()
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}

// Fragment — just annotate with @AndroidEntryPoint
@AndroidEntryPoint
class OrderFragment : Fragment() {
    val viewModel: OrderViewModel by viewModels()   // Hilt provides it
}

// Activity-scoped shared ViewModel
@AndroidEntryPoint
class CheckoutFragment : Fragment() {
    val cartVm: CartViewModel by activityViewModels()
}

// Assisted injection — for dynamic parameters at creation time
@HiltViewModel(assistedFactory = DetailViewModel.Factory::class)
class DetailViewModel @AssistedInject constructor(
    @Assisted val itemId: String,  // provided at runtime
    private val repo: ItemRepository  // from DI graph
) : ViewModel() {
    @AssistedFactory
    interface Factory {
        fun create(itemId: String): DetailViewModel
    }
}
@HiltViewModel
On the ViewModel class
Marks the class for Hilt code generation. Enables @Inject constructor with full DI graph access. Requires the owning Activity/Fragment to be @AndroidEntryPoint.
@AndroidEntryPoint
On Activity / Fragment
Injects an HiltViewModelFactory as the default factory. This factory intercepts ViewModel creation requests and routes them through the Hilt DI graph when the class has @HiltViewModel.
SavedStateHandle
Auto-injected
Hilt automatically provides a SavedStateHandle to any @HiltViewModel that requests it. No configuration needed — just add it to the constructor.
@AssistedInject
Lifecycle 2.6+
For ViewModels that need runtime parameters (like an item ID) alongside injected dependencies. Hilt generates a factory that accepts the assisted parameters while providing the rest from the DI graph.
09 · Process Death

SavedStateHandle

ViewModel survives rotation but not process death. When Android kills your process to reclaim memory, the entire JVM is gone — including the ViewModelStore. SavedStateHandle bridges this gap: it's backed by the same Bundle mechanism as onSaveInstanceState, so its contents survive process death and are restored when the user returns.

The rule of thumb: Everything your ViewModel needs to reconstruct the UI after process death should be in SavedStateHandle. Everything else — large datasets, lists, complex objects — stays in the ViewModel's normal fields (or in Room/DataStore for true persistence).

SavedStateHandle — complete patterns
@HiltViewModel
class SearchViewModel @Inject constructor(
    private val handle: SavedStateHandle,
    private val searchRepo: SearchRepository
) : ViewModel() {

    // Pattern 1: Simple get/set — survives rotation + process death
    var query: String
        get() = handle["query"] ?: ""
        set(v) { handle["query"] = v }

    // Pattern 2: StateFlow backed by SavedStateHandle
    // ← this is the recommended modern pattern
    val queryFlow: StateFlow<String> =
        handle.getStateFlow("query", "")

    // Setting updates the StateFlow AND persists to bundle
    fun onQueryChanged(q: String) {
        handle["query"] = q   // triggers queryFlow emission
    }

    // Pattern 3: Drive a search from the persisted query
    val results: StateFlow<SearchState> =
        handle.getStateFlow("query", "")
            .debounce(300)
            .distinctUntilChanged()
            .flatMapLatest { q ->
                if (q.isBlank()) flowOf(SearchState.Empty)
                else searchRepo.search(q).map { SearchState.Results(it) }
            }
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), SearchState.Empty)

    // Pattern 4: Reading Navigation arguments automatically
    // Navigation passes destination args to SavedStateHandle
    val orderId: String = handle.get<String>("orderId")!!
    // Or with the generated nav args type
    val args = OrderDetailArgs.fromSavedStateHandle(handle)
}
Storage Survives rotation Survives process death Size limit Best for
ViewModel fields Yes No Unlimited Lists, complex objects, in-flight calls
SavedStateHandle Yes Yes ~1MB (Bundle) Search query, scroll position, selected ID
SharedPreferences / DataStore Yes Yes Unlimited User preferences, settings
Room database Yes Yes Device storage Domain entities, offline data
10 · Best Practices

Patterns & Pitfalls

Never hold a Context in a ViewModel. Activities and Fragments are Contexts, and they're recreated on every rotation. A ViewModel holding a reference to an old Activity = memory leak guaranteed. If you need an Application context, use AndroidViewModel (which holds application: Application) or inject the Application via Hilt.

Never hold View or Fragment references in a ViewModel. Same reason — Views and Fragments are destroyed on rotation. The ViewModel observes data; the UI observes the ViewModel. The arrow of dependency always points upward, never down.

Always use viewModelScope for coroutines. It's tied to onCleared() and automatically cancelled. This prevents coroutine leaks from in-flight network calls after the user has navigated away.

Use StateFlow (not LiveData) for new code. StateFlow works correctly in Compose with collectAsStateWithLifecycle(), supports operators, and is Kotlin-native. LiveData is still fine for View-based apps but is no longer recommended for greenfield.

The complete production pattern

ProductionViewModel.kt — the full template
@HiltViewModel
class ArticleViewModel @Inject constructor(
    private val repo: ArticleRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    // ── State (UI observable) ──────────────────────────────────
    val uiState: StateFlow<ArticleUiState> =
        savedState.getStateFlow("filter", Filter.All)
            .flatMapLatest { filter -> repo.getArticles(filter) }
            .map { articles -> ArticleUiState.Success(articles) }
            .catch { e -> emit(ArticleUiState.Error(e.message)) }
            .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ArticleUiState.Loading)

    // ── One-shot events (navigation, snackbar) ─────────────────
    private val _events = MutableSharedFlow<ArticleEvent>(extraBufferCapacity = 1)
    val events = _events.asSharedFlow()

    // ── User actions ───────────────────────────────────────────
    fun onFilterChanged(filter: Filter) {
        savedState["filter"] = filter
    }

    fun onArticleClicked(article: Article) {
        viewModelScope.launch {
            _events.emit(ArticleEvent.Navigate("articles/${article.id}"))
        }
    }

    fun onBookmarkClicked(article: Article) {
        viewModelScope.launch {
            repo.toggleBookmark(article.id)
            _events.tryEmit(ArticleEvent.ShowSnackbar("Bookmarked"))
        }
    }
}

// ── Sealed UI state ────────────────────────────────────────────
sealed class ArticleUiState {
    data object Loading : ArticleUiState()
    data class Success(val articles: List<Article>) : ArticleUiState()
    data class Error(val message: String?) : ArticleUiState()
}

// ── Fragment collection ────────────────────────────────────────
viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch {
            viewModel.uiState.collect { state ->
                when (state) {
                    is ArticleUiState.Loading  -> showLoading()
                    is ArticleUiState.Success  -> adapter.submitList(state.articles)
                    is ArticleUiState.Error    -> showError(state.message)
                }
            }
        }
        launch {
            viewModel.events.collect { event ->
                when (event) {
                    is ArticleEvent.Navigate     -> navigate(event.route)
                    is ArticleEvent.ShowSnackbar -> showSnackbar(event.msg)
                }
            }
        }
    }
}

Testing ViewModels

ViewModels are pure Kotlin — no Android framework dependencies (if you avoid AndroidViewModel). This makes them straightforward to unit test with coroutines and fakes.

ArticleViewModelTest.kt
@OptIn(ExperimentalCoroutinesApi::class)
class ArticleViewModelTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    private val fakeRepo = FakeArticleRepository()
    private val savedState = SavedStateHandle()

    private lateinit var viewModel: ArticleViewModel

    @Before
    fun setup() {
        viewModel = ArticleViewModel(fakeRepo, savedState)
    }

    @Test
    fun `filter change loads correct articles`() = runTest {
        val states = mutableListOf<ArticleUiState>()
        val job = launch(UnconfinedTestDispatcher()) {
            viewModel.uiState.toList(states)
        }
        viewModel.onFilterChanged(Filter.Bookmarked)
        assertEquals(Filter.Bookmarked, fakeRepo.lastFilter)
        job.cancel()
    }
}
The mental model
ViewModelStoreOwner
Any class that holds a ViewModelStore and manages its lifetime. Activity, Fragment, NavBackStackEntry, or your custom class.
ViewModelStore
A simple HashMap<String, ViewModel> that the Owner retains across configuration changes and clears when truly done.
ViewModelProvider
The gatekeeper. Gets from Store if exists; creates via Factory and puts in Store if not. Keys by class name + optional custom key.
ViewModel
Lives in the Store. Survives configuration changes. Destroyed only when Owner permanently ends. SavedStateHandle crosses the process-death boundary.